const KdocsDrive = require("./index").KdocsDrive;
const CommonAxerrHG = require("../../tool/CommonAxerrHandlerGenerator").CommonAxerrHandlerGen;
const util = require("util");
const FormData = require("form-data");
const fs = require("fs");
const rn = require("random-number");
const path = require("path");
// const util = require("util");
const rs = require("randomstring");
const buffer = require("buffer");
const runParallel = require("run-parallel-limit");
const Toolbox = require("../../tool/toolbox")
const NWER = require("../NETWORKERR_RETRY");

class ModelFileOrFolderDownloadable {
  /**
   * 
   * @param {Strin} fname 
   * @param {Number} id 
   * @param {Number} parentid 
   * @param {Number} groupid 
   * @param {Number} fsize 如果type是文件夹,size=0
   * @param {"file"|"folder"} ftype 
   * @param {Number} fver 当前version,初始为1
   * @param {Number} mtime 最后修改时间,被除以过1000的时间戳
   * @param {Number} ctime 创建时间,被除以过1000的时间戳
   * @param {Boolean} deleted 
   */
  constructor(fname, id, parentid, groupid, fsize, ftype, fver, mtime, ctime, deleted) {
    this.fname = fname;
    this.id = id;
    this.parentid = parentid;
    this.groupid = groupid;
    this.fsize = fsize;
    this.ftype = ftype;
    this.fver = fver;
    this.mtime = mtime;
    this.ctime = ctime;
    this.deleted = deleted;
  }
}

class SpecialGroup {
  /**
   * @param {Number} id
   * @param {string} name
   * @param {KdocsDrive} kdocs 
   */
  constructor(kdocs, id, name) {
    this.kdocs = kdocs;
    this.name = name;
    this.id = id;

    this.app = new SP_Application(this);

    let _this = this;
    (function 容易网络错误的给我重试() {
      for (let funcname of ['getKs3Token',
        'listDownloadableItemsInFolder',
        'listPathTo',
        'getDownloadLink',
        'createFile_newfile',
        'createFile_new_version',
        'createFolder',
      ]) {
        let f = NWER.NewFuncRetry(_this, funcname);
        _this[funcname] = f;
      }
    })();
  }

  get axios() {
    return this.kdocs.axios;
  }

  /**
 * @returns {Promise<{ok:Boolean,msg:String,data:{
  * KSSAccessKeyId: string,
  * Policy: string
  * Signature: string,
  * key: String,
  * url: String
  * "x-kss-newfilename-in-body": String,
  * "x-kss-server-side-encryption": String
  * }}>}
  * @param {String} name
  */
  getKs3Token(name, size = 0, parent_id = 0) {
    if (!size) {
      size = parseInt(Math.random() * 1000)
    }
    return new Promise(resolve => {
      this.axios.get("https://www.kdocs.cn/3rd/drive/api/files/upload/request", {
        params: {
          groupid: this.id,
          parentid: parent_id,
          size: size,
          name: name ? (path.extname(name) ? `${Math.random()}.${path.extname(name)}` : `${Math.random()}.pptx`) : Math.random() + ".docx",
          store: "ks3",
          method: "POST",
          encrypt: "true"
        },
        headers: {
          cookie: this.kdocs.cookies_as_header,
          
        }
      }).then(axresp => {
        if (axresp.data
          && axresp.data.result == "ok"
          && axresp.data.key
          && axresp.data.KSSAccessKeyId
          && axresp.data.Policy
          && axresp.data.Signature
          && axresp.data.url
          && axresp.data["x-kss-newfilename-in-body"]
          && axresp.data["x-kss-server-side-encryption"]) {
          console.log(name, axresp.data.url);
          return resolve({
            ok: true,
            msg: 'ok',
            data: {
              key: axresp.data.key,
              KSSAccessKeyId: axresp.data.KSSAccessKeyId,
              Policy: axresp.data.Policy,
              Signature: axresp.data.Signature,
              url: axresp.data.url,
              "x-kss-newfilename-in-body": axresp.data["x-kss-newfilename-in-body"],
              "x-kss-server-side-encryption": axresp.data["x-kss-server-side-encryption"]
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
  * @return {Promise<{ok:Boolean,msg:String,data:{newfilename:String,etag:String,using_url:String}}>}
   * @param {String} file_path 必须是小于1GB的文件
   * @param {String} fake_extname
   */
  uploadFileToKs3(file_path, fake_extname = "") {
    /**
     * @returns {Promise<{ok:Boolean,msg:String,data:{newfilename:String,etag:String,using_url:String}}>}
     */
    let upload = () => {
      return new Promise(async resolve => {
        let stream = fs.createReadStream(file_path)
        let o_token = await this.getKs3Token(fake_extname ? `${Math.random()}.${fake_extname}` : `${Math.random()}.${path.basename(file_path)}`);
        if (!o_token.ok) {
          return resolve({
            ok: false,
            msg: `get token fail:${o_token.msg}`
          })
        }
        let form = new FormData;
        form.append("key", o_token.data.key);
        form.append("Policy", o_token.data.Policy);
        form.append("submit", "Upload to KS3");
        form.append("Signature", o_token.data.Signature);
        form.append("KSSAccessKeyId", o_token.data.KSSAccessKeyId);
        form.append("x-kss-newfilename-in-body", o_token.data["x-kss-newfilename-in-body"]);
        form.append("x-kss-server-side-encryption", o_token.data["x-kss-server-side-encryption"]);
        form.append("file", stream, {
          filename: Math.random() + "." + path.extname(file_path),
          contentType: "application/octet-stream"
        });

        let url = require("url");
        let using_url = o_token.data.url
        let parsed = url.parse(using_url);
        let upload_has_response = false;
        form.submit({
          host: parsed.host,
          path: parsed.path,
          headers: {
            Origin: 'https://www.kdocs.cn',
            Referer: 'https://www.kdocs.cn/?show=all',
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"
          },
          protocol: parsed.protocol
        }, (err, response) => {
          if (err) {
            return resolve({
              ok: false,
              msg: `form.submit() error :${err.message}`
            });
          }
          response.on("error", (err) => resolve({
            ok: false,
            msg: `response.on(error):${err.message}`
          }));
          if (!response.headers['content-type']) {
            return resolve({
              ok: false,
              msg: `no response content-type`
            });
          }
          if (!(response.headers['content-type'].startsWith("application/json"))) {
            let buf = buffer.Buffer.from("")
            response.on("data", (d) => buf += d);

            return setTimeout(_ => resolve({
              ok: false,
              msg: `response content-type is ${response.headers['content-type']},data is :\n${buf.toString()}`
            }), 10);
          }
          if (response.statusCode !== 200) {
            return resolve({
              ok: false,
              msg: `response.statusCode is ${response.statusCode}`
            });
          }
          if (!(response.headers.etag && response.headers.newfilename)) {
            return resolve({
              ok: false,
              msg: `response.headers don't have etag and newfilename: ${util.inspect(response.headers)}`
            });
          }
          let etag = response.headers.etag;
          etag = etag.replace(/\"/g, "");
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              etag: etag,
              using_url: using_url,
              newfilename: response.headers.newfilename,
            }
          })
        });
        form.on("end", () => {
          setTimeout(_ => {
            if (!upload_has_response) {
              resolve({
                ok: false,
                msg: '[LATE RESPONSE]no HTTP response after form.end 60s'
              })
            }
          }, 60 * 1000)
        });

      })
    }


    return new Promise(async resolve_upload => {
      let x = 5;
      let errs = [];
      for (let i = 1; i <= x; i++) {
        let otry = await upload();
        if (otry.ok) {
          return resolve_upload({
            ok: true,
            msg: "ok",
            data: {
              newfilename: otry.data.newfilename,
              etag: otry.data.etag,
              using_url: otry.data.using_url
            }
          })
        } else {
          errs.push(otry.msg)
        }
      }
      return resolve_upload({
        ok: false,
        msg: `X=${x} try fail:${errs.join(" ; ")}`
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{
   * files:Array<ModelFileOrFolderDownloadable>,
   * next_offset:Number}}>} next_offset为-1表示没有下一页了
   * @param {Number} offset 从0数起
   * @param {Number} result_size 这次api请求最多返回的文件(夹)个数 默认20
   * @param {Number} folder_id 
   * @param {"mtime"|"fname"} orderby
   * @param {"DESC"|"ASC"} order
   */
  listDownloadableItemsInFolder(folder_id = 0, offset = 0, result_size = 20, orderby = "fname", order = "ASC") {
    return new Promise(resolve => {
      this.axios.get(`https://www.kdocs.cn/3rd/drive/api/v5/groups/${
        this.id
        }/files`, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
        },

        params: {
          linkgroup: true,
          offset: offset,
          count: result_size,
          orderby: orderby,
          order: order,
          append: false,
          parentid: folder_id
        }
      }).then(axresp => {
        if (axresp.data
          && axresp.data.result == "ok"
          && util.isArray(axresp.data.files)) {
          /**@type {Array<ModelFileOrFolderDownloadable>} */
          let models = [];
          for (let f of axresp.data.files) {
            if (f['ftype'] == "file") {
              models.push(new ModelFileOrFolderDownloadable(f['fname'],
                f['id'],
                f['parentid'], f['groupid'], f['fsize'], f['ftype'], f['fver'],
                f['mtime'], f['ctime'], f['deleted'])
              )
            }
            if (f['ftype'] == "folder") {
              models.push(new ModelFileOrFolderDownloadable(f['fname'],
                f['id'],
                f['parentid'], f['groupid'], f['fsize'], f['ftype'], f['fver'],
                f['mtime'], f['ctime'], f['deleted'])
              )
            }

          }
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              files: models,
              next_offset: axresp.data.next_offset
            }
          })
        }
        debugger


        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr)
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,
   * data:{
   * path:Array<{
   * fname:String,
   * fileid:Number,
   * type:"folder"|"file"
   * }>
   * }}>}
   * @param {Number} file_or_folder_id 
   */
  listPathTo(file_or_folder_id) {
    return new Promise(resolve => {
      this.axios.get(`https://www.kdocs.cn/3rd/drive/api/v5/groups/${
        this.id
        }/files/${
        file_or_folder_id
        }/path`, {
        headers: {
          cookie: this.kdocs.cookies_as_header
        }
      }).then(axresp => {
        if (axresp.data && util.isArray(axresp.data['path'])) {
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              path: axresp.data['path'].map(e => {
                return {
                  fname: e.fname,
                  fileid: e.fileid,
                  type: e.type
                }
              })
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{store:String,link:String}}>}
   * @param {Number} file_id 
   */
  getDownloadLink(file_id) {
    return new Promise(resolve => {
      this.axios.get(`https://www.kdocs.cn/3rd/drive/api/v3/groups/${
        this.id
        }/files/${
        file_id}/download?isblocks=false`, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: `https://www.kdocs.cn/?show=all`
        }
      }).then(axresp => {
        if (axresp.data
          && axresp.data['fileinfo']
          && axresp.data['fileinfo']['url']
          && axresp.data['fileinfo']['store']) {
          return resolve({
            ok: true,
            msg: 'ok',
            data: {
              store: axresp.data['fileinfo']['store'],
              link: axresp.data['fileinfo']['static_url'] || axresp.data['fileinfo']['url']
            }
          })
        }

        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @deprecated
   * @returns {Promise<{ok:Boolean,msg:String,data:{
   * fver:Number,
   * id:Number,
   * groupid:Number,
   * parentid:Numberm
   * fsize:Number,
   * fname:String}}>}
   * @param {import("fs").ReadStream|string|Buffer} stream 
   * @param {Number} folder_id 
   * @param {Number} fake_size 
   * @param {string} file_name
   */
  uploadNewFileToFolder_deprecated(stream, file_name, folder_id = 0, fake_size = 1) {
    /**
     * @returns {Promise<{ok:Boolean,msg:String,data:{
     * KSSAccessKeyId: string,
     * Policy: string
     * Signature: string,
     * key: String,
     * url: String
     * "x-kss-newfilename-in-body": String,
     * "x-kss-server-side-encryption": String
     * }}>}
     */
    let getKs3Token = () => {
      return new Promise(resolve => {
        this.axios.get("https://www.kdocs.cn/3rd/drive/api/files/upload/request", {
          params: {
            groupid: this.id,
            parentid: folder_id,
            size: fake_size,
            name: file_name,
            store: "ks3",
            method: "POST",
            encrypt: "true"
          },
          headers: {
            cookie: this.kdocs.cookies_as_header
          }
        }).then(axresp => {
          if (axresp.data
            && axresp.data.result == "ok"
            && axresp.data.key
            && axresp.data.KSSAccessKeyId
            && axresp.data.Policy
            && axresp.data.Signature
            && axresp.data.url
            && axresp.data["x-kss-newfilename-in-body"]
            && axresp.data["x-kss-server-side-encryption"]) {
            return resolve({
              ok: true,
              msg: 'ok',
              data: {
                key: axresp.data.key,
                KSSAccessKeyId: axresp.data.KSSAccessKeyId,
                Policy: axresp.data.Policy,
                Signature: axresp.data.Signature,
                url: axresp.data.url,
                "x-kss-newfilename-in-body": axresp.data["x-kss-newfilename-in-body"],
                "x-kss-server-side-encryption": axresp.data["x-kss-server-side-encryption"]
              }
            })
          }
          throw axresp.data;
        }).catch(axerr => {
          CommonAxerrHG(resolve)(axerr);
        })
      })
    };

    /**
     * @returns {Promise<{ok:Boolean,msg:String,data:{newfilename:String,etag:String}}>}
     */
    let upload = () => {
      return new Promise(async resolve => {
        let o_token = await getKs3Token();
        if (!o_token.ok) {
          return resolve({
            ok: false,
            msg: `get token fail:${o_token.msg}`
          })
        }
        let form = new FormData;
        form.append("key", o_token.data.key);
        form.append("Policy", o_token.data.Policy);
        form.append("submit", "Upload to KS3");
        form.append("Signature", o_token.data.Signature);
        form.append("KSSAccessKeyId", o_token.data.KSSAccessKeyId);
        form.append("x-kss-newfilename-in-body", o_token.data["x-kss-newfilename-in-body"]);
        form.append("x-kss-server-side-encryption", o_token.data["x-kss-server-side-encryption"]);
        form.append("file", stream, {
          filename: file_name,
          contentType: "application/octet-stream"
        });

        let url = require("url");
        let parsed = url.parse(o_token.data.url);
        let upload_has_response = false;
        form.submit({
          host: parsed.host,
          path: parsed.path,
          headers: {
            Origin: 'https://www.kdocs.cn',
            Referer: 'https://www.kdocs.cn/?show=all',
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"
          },
          protocol: parsed.protocol
        }, (err, response) => {
          if (err) {
            return resolve({
              ok: false,
              msg: `form.submit() error :${err.message}`
            });
          }
          response.on("error", (err) => resolve({
            ok: false,
            msg: `response.on(error):${err.message}`
          }));
          if (!response.headers['content-type']) {
            return resolve({
              ok: false,
              msg: `no response content-type`
            });
          }
          if (!(response.headers['content-type'].startsWith("application/json"))) {
            return resolve({
              ok: false,
              msg: `response content-type is ${response.headers['content-type']}`
            });
          }
          if (response.statusCode !== 200) {
            return resolve({
              ok: false,
              msg: `response.statusCode is ${response.statusCode}`
            });
          }
          if (!(response.headers.etag && response.headers.newfilename)) {
            return resolve({
              ok: false,
              msg: `response.headers don't have etag and newfilename: ${util.inspect(response.headers)}`
            });
          }
          let etag = response.headers.etag;
          etag = etag.replace(/\"/g, "");
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              etag: etag,
              newfilename: response.headers.newfilename
            }
          })
        });
        form.on("end", () => {
          setTimeout(_ => {
            if (!upload_has_response) {
              resolve({
                ok: false,
                msg: '[LATE RESPONSE]no HTTP response after form.end 60s'
              })
            }
          }, 60 * 1000)
        });

      })
    }

    /**
     * @param {Number} x
 * @returns {Promise<{ok:Boolean,msg:String,data:{newfilename:String,etag:String}}>}
 */
    let upload_xtry = (x) => new Promise(async resolve_upload => {
      let errs = [];
      for (let i = 1; i <= x; i++) {
        let otry = await upload();
        if (otry.ok) {
          return resolve_upload({
            ok: true,
            msg: "ok",
            data: {
              newfilename: otry.data.newfilename,
              etag: otry.data.etag
            }
          })
        } else {
          errs.push(otry.msg)
        }
      }
      return resolve_upload({
        ok: false,
        msg: `X=${x} try fail:${errs.join(" ; ")}`
      })
    })

    return new Promise(async resolve => {
      let o_upload = await upload_xtry();
      if (!o_upload.ok) {
        return resolve({
          ok: false,
          msg: `upload() fail:${o_upload.msg}`
        })
      }
      // debugger
      let apiPoint = 'https://www.kdocs.cn/3rd/drive/api/v5/files/file'
      apiPoint = `https://drive.wps.cn/api/v3/groups/${this.id}/files`
      this.axios.post(apiPoint, {
        "groupid": this.id,
        "parentid": folder_id,
        "parent_path": [],
        "name": file_name,
        "isUpNewVer": false,
        // "etag": o_upload.data.etag,
        "store": "ks3",
        "size": fake_size,
        "sha1": o_upload.data.newfilename,
        "csrfmiddlewaretoken": this.kdocs.csrf_token
      }, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: "https://www.kdocs.cn/?show=all",
          origin: 'https://www.kdocs.cn',
          'content-type': 'application/json;charset=UTF-8'
        }
      }).then(axresp => {
        // debugger
        if (axresp.data
          && axresp.data.ctime) {
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              fver: axresp.data.fver,
              groupid: axresp.data.groupid,
              id: axresp.data.id,
              fname: axresp.data.fname,
              fsize: axresp.data.fsize,
              parentid: axresp.data.parentid
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        // debugger
        CommonAxerrHG(resolve)(axerr)
      })
    })
  }

  /**
   * @description id是history的id，当id为0时表示最新版本；fver是越新的版本越大，最初是fver=1
   * @return {Promise<{ok:Boolean,msg:String,
   * data:{
   * histories:Array<{
   * fver:Number,
   * id:Number,
   * groupid:Number,
   * fsize:Number,
   * fname:String,
   * fsha:String,
   * parentid:Number,
   * fileid:String
   * }>
   * }}>}
   * @param {Number} file_id 
   * @param {Number} offset 
   * @param {Number} count 
   */
  getFileHistory(file_id, offset = 0, count = 20) {
    return new Promise(resolve => {
      this.axios.get(`https://www.kdocs.cn/3rd/drive/api/v3/files/${
        file_id
        }/histories?offset=${
        offset
        }&count=${
        count
        }&groupid=${this.id}`, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: "https://www.kdocs.cn/?from=docs&show=all"
        }
      }).then(axresp => {
        if (axresp.data
          && axresp.data['result']
          && util.isArray(axresp.data['histories'])) {
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              histories: axresp.data['histories'].map(e => {
                return {
                  fver: e.fver,
                  id: e.id,
                  groupid: e.groupid,
                  fsize: e.fsize,
                  fname: e.fname,
                  fsha: e.fsha,
                  parentid: e.parentid,
                  fileid: e.fileid
                }
              })
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr)
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{
   * file_id:Number,
   * user_id:Number,
   * file_name:String,
   * file_size:Number,
   * modified_time:Date,
   * store_type:"ks3",
   * version:Number,
   * sha1:String
   * }}>}
   * @param {Number} file_id 
   * @param {Number} history_id 
   */
  getHistoryInfo(file_id, history_id) {
    return new Promise(resolve => {
      this.axios.get(`https://www.kdocs.cn/3rd/drive/api/v3/files/${
        file_id
        }/histories/${
        history_id
        }?groupid=${this.id}`, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: `https://www.kdocs.cn/history/${this.id}/${file_id}/${history_id}`
        }
      }).then(axresp => {
        if (axresp.data && axresp.data.history) {
          let file_id = axresp.data.history['Id'];
          let user_id = axresp.data.history['UserId'];
          let file_name = axresp.data.history['Name'];
          let file_size = axresp.data.history['Size'];
          let modified_time = new Date(axresp.data.history['MTime'] * 1000);
          let store_type = axresp.data.history['StoreType'];
          let version = axresp.data.history['Version'];
          let sha1 = axresp.data.history['HexSha1'];
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              file_id: file_id,
              user_id,
              file_name,
              file_size,
              modified_time,
              store_type,
              version,
              sha1
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{
   * fname:String,
   * fsha:String,
   * fsize:Number,
   * fver:Number
   * id:Number
   * }}>}
   * @param {Number} parent_id 
   * @param {String} define_filename 据说不能大于240字符&&不允许.gitignore这样的名字
   * @param {String} ks3_newfilename 
   * @param {String} ks3_etag 
   * @param {Number} size
   */
  createFile_newfile(parent_id, define_filename, ks3_newfilename, ks3_etag, size) {
    while(ks3_etag.includes("\"")){
      ks3_etag = ks3_etag.replace("\"","")
    }
    return new Promise(resolve => {
      // debugger
      this.axios.post(`https://www.kdocs.cn/3rd/drive/api/v5/files/file`, {
        csrfmiddlewaretoken: this.kdocs.csrf_token,
        etag: ks3_etag,
        groupid: this.id,
        isUpNewVer: false,
        name: define_filename,
        parent_path: [],
        parentid: String(parent_id),
        sha1: ks3_newfilename,
        size: size || rn({ max: 1000, min: 1, integer: true }),
        store: "ks3"
      }, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: `https://www.kdocs.cn/mine/${parent_id}`,
          origin: 'https://www.kdocs.cn',
          'content-type': 'application/json;charset=UTF-8'
        },
        validateStatus: s => s == 200 || s == 403
      }).then(async axresp => {
        if (axresp.status == 403) {
          // if (axresp.data['result'].toLowerCase() == "fileNotUploaded".toLowerCase()) {
          //   debugger
          //   await new Promise(r => setTimeout(r, 2000));
          //   let anothertry = await this.createFile_newfile(parent_id, define_filename,
          //     ks3_newfilename, ks3_etag, size);
          //   return resolve(anothertry);
          // }
          throw axresp;
        }
        if (axresp.data && axresp.data['ctime']) {
          let fver = axresp.data['fver'];
          if (fver > 1) {
            return resolve({
              ok: false,
              msg: `fver is NOT 1!!${JSON.stringify(axresp.data)}`
            })
          }
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              fname: axresp.data.fname,
              fsha: axresp.data.fsha,
              fsize: axresp.data.fsize,
              id: axresp.data.id,
              fver: fver
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr)
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{
    * fname:String,
    * fsha:String,
    * fsize:Number,
    * fver:Number
    * id:Number
    * }}>}
   * @param {String} file_id_str 
   * @param {String} ks3_newfilename 
   * @param {String} ks3_etag 
   */
  createFile_new_version(file_id_str, ks3_newfilename, ks3_etag) {
    return new Promise(resolve => {
      this.axios.put(`https://www.kdocs.cn/3rd/drive/api/v5/files/file`, {
        fileid: file_id_str,
        csrfmiddlewaretoken: this.kdocs.csrf_token,
        etag: ks3_etag,
        sha1: ks3_newfilename,
        size: 1,
        store: "ks3",
      }, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: "https://www.kdocs.cn/?show=all",
          origin: 'https://www.kdocs.cn',
          'content-type': 'application/json;charset=UTF-8'
        }
      }).then(axresp => {
        if (axresp.data && axresp.data['ctime']) {
          let fver = axresp.data['fver'];
          if (fver <= 1) {
            return resolve({
              ok: false,
              msg: `fver <= 1!!${JSON.stringify(axresp.data)}`
            })
          }
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              fname: axresp.data.fname,
              fsha: axresp.data.fsha,
              fsize: axresp.data.fsize,
              id: axresp.data.id,
              fver: fver
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr)
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,
   * data:{
   * fname:String,
   * id:Number
   * }}>}
   * @param {String} name 
   * @param {Number} parent_id 
   */
  createFolder(name, parent_id = 0) {
    return new Promise(resolve => {
      this.axios.post('https://www.kdocs.cn/3rd/drive/api/v5/files/folder', {
        csrfmiddlewaretoken: this.kdocs.csrf_token,
        groupid: String(this.id),
        name: name,
        owner: true,
        parentid: parent_id,
        parsed: true
      }, {
        validateStatus: s => s == 200 || s == 403,
        headers: {
          cookie: this.kdocs.cookies_as_header,
          'content-type': 'application/json;charset=UTF-8'
        }

      }).then(axresp => {
        if (axresp.status == 403) {
          if (axresp.data && axresp.data.result
            && axresp.data.result == "fileNameConflict") {
            return resolve({
              ok: false,
              msg: "NAME_CONFLICT"
            })
          }
          throw axresp;
        }
        if (axresp.status == 200 && axresp.data
          && axresp.data['ftype'] == "folder") {
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              fname: axresp.data.fname,
              id: axresp.data.id
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @description 注意新的名字必须和原来的同样的扩展名！！！
   * @returns {Promise<{ok:Boolean,msg:String}>}
   * @param {Number} file_id_of_file_or_dir  文件或文件夹的id
   * @param {string} newname
   */
  changeNameOf(file_id_of_file_or_dir, newname) {
    if (newname.length > 240) {
      newname = newname.substr(0, 240);
    }
    return new Promise(resolve => {
      this.axios.put(`https://www.kdocs.cn/3rd/drive/api/v3/groups/${
        this.id
        }/files/${file_id_of_file_or_dir}`, {
        csrfmiddlewaretoken: this.kdocs.csrf_token,
        fname: newname
      }, {
        headers: {
          cookie: this.kdocs.cookies_as_header,
          referer: "https://www.kdocs.cn/?show=all",
          origin: 'https://www.kdocs.cn',
          'content-type': 'application/json;charset=UTF-8'
        }
      }).then(axresp => {
        if (axresp.data['id']) {
          return resolve({
            ok: true,
            msg: "ok"
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String}>}
   * @param {Number[]} fileids 
   * @param {Number} their_parent_id 
   * @param {Number} targetdir_id 
   */
  move_insideThisGroup(fileids, their_parent_id, targetdir_id) {
    return new Promise(resolve => {
      this.axios.post(`https://www.kdocs.cn/3rd/drive/api/v3/groups/${this.id
        }/files/batch/move`,
        {
          "fileids": fileids,
          "groupid": this.id,
          "parentid": their_parent_id,
          "target_groupid": this.id,
          "target_parentid": targetdir_id,
          "csrfmiddlewaretoken": this.kdocs.csrf_token
        },
        {
          headers: {
            cookie: this.kdocs.cookies_as_header,
            referer: `https://www.kdocs.cn/mine/${their_parent_id}`,
            origin: 'https://www.kdocs.cn',
            'content-type': 'application/json;charset=UTF-8'
          }
        }
      ).then(axresp => {
        if (axresp.data['result'] === "ok") {
          return resolve({
            ok: true,
            msg: "ok"
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String}>}
   * @param {Number[]} fileids 
   * @param {Number} their_parent_id 
   */
  delete(fileids, their_parent_id) {
    return new Promise(resolve => {
      this.axios.post(`https://www.kdocs.cn/3rd/drive/api/v3/groups/${this.id
        }/files/batch/delete`, {
        "fileids": fileids,
        "groupid": String(this.id),
        "sourceid": String(their_parent_id),
        "csrfmiddlewaretoken": this.kdocs.csrf_token
      },
        {
          headers: {
            cookie: this.kdocs.cookies_as_header,
            referer: "https://www.kdocs.cn/?show=all",
            origin: 'https://www.kdocs.cn',
            'content-type': 'application/json;charset=UTF-8'
          }
        }).then(axresp => {
          if (axresp.data['result'] === "ok") {
            return resolve({
              ok: true, msg: "ok"
            })
          }
          throw axresp.data;
        })
        .catch(axerr => {
          CommonAxerrHG(resolve)(axerr);
        })
    })

  }



}

class SP_Application {
  /**
   * 
   * @param {SpecialGroup} special_group 
   */
  constructor(special_group) {
    this.special_group = special_group;
  }

  /**
   * @return {Promise<{ok:Boolean,msg:String,
   * data:{
    * fver:Number,
    * id:Number,
    * groupid:Number,
    * fsize:Number,
    * fname:String,
    * fsha:String,
    * parentid:Number,
    * fileid:String
   * }}>}
   * @param {Number} file_id 
   */
  getFirstVersionOf(file_id) {
    return new Promise(async resolve => {
      let o_firstTry = await this.special_group.getFileHistory(file_id, 0, 20);
      if (!o_firstTry.ok) {
        return resolve({
          ok: false,
          msg: `first try fail:${o_firstTry.msg}`
        })
      }
      let find1Ver = o_firstTry.data.histories.find(e => e.fver == 1);
      if (find1Ver) {
        return resolve({
          ok: true,
          msg: "ok",
          data: find1Ver
        })
      }
      let lastVer = o_firstTry.data.histories[0].fver;
      let o_second = await this.special_group.getFileHistory(file_id,
        lastVer - 1, 20);
      if (!o_second.ok) {
        return resolve({
          ok: false,
          msg: `second try fail:${o_second.msg}`
        })
      }
      find1Ver = o_second.data.histories.find(e => e.fver == 1);
      if (find1Ver) {
        return resolve({
          ok: true,
          msg: "ok",
          data: find1Ver
        })
      }
      else {
        return resolve({
          ok: false,
          msg: `second 居然没有搞到fver=1的记录？？：${JSON.stringify(o_second.data.histories)}`
        })
      }

    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{link:String}}>}
   * @param {Number} file_id 
   * @param {Number} history_id 
   */
  getHistoryDownloadLink(file_id, history_id) {
    return new Promise(async resolve => {
      if (history_id == 0) {
        let _zuixin = await this.special_group.getDownloadLink(file_id);
        if (!_zuixin.ok) {
          return resolve({
            ok: false,
            msg: _zuixin.msg
          })
        } else {
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              link: _zuixin.data.link
            }
          })
        }
      }
      this.special_group.axios.get(`https://www.kdocs.cn/3rd/drive/api/v3/files/${
        file_id
        }/histories/${
        history_id
        }/download?groupid=${this.special_group.id}`, {
        headers: {
          cookie: this.special_group.kdocs.cookies_as_header,
          referer: `https://www.kdocs.cn/history/${this.special_group.id}/${file_id}/${history_id}`
        }
      }).then(axresp => {
        if (axresp.data
          && axresp.data['fileinfo']
          && axresp.data['fileinfo']['url']) {
          return resolve({
            ok: true,
            msg: "ok",
            data: {
              link: axresp.data['fileinfo']['url']
            }
          })
        }
        throw axresp.data;
      }).catch(axerr => {
        CommonAxerrHG(resolve)(axerr);
      })
    })
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,
    * data:{
      * fname:String,
      * id:Number
      * }}>}
   * @param {String} folder_name 
   * @param {Number} parent_id 
   */
  createFolder_GoodNoConflict(folder_name, parent_id) {
    let name = folder_name;
    if (folder_name.length > 240) {
      name = `${folder_name.substr(0, 235)}.${rs.generate(4)}`
    }
    return new Promise(async resolve => {
      let first = await this.special_group.createFolder(name, parent_id);
      if (first.ok) {
        return resolve({
          ok: true,
          msg: "ok",
          data: {
            fname: first.data.fname,
            id: first.data.id
          }
        })
      }
      if (first.msg.includes("NAME_CONFLICT")) {
        let next_name = `${name}.${rs.generate(5)}`;
        if (next_name.length > 240) {
          next_name = `${next_name.substr(0, 235)}.${rs.generate(4)}`
        }
        let second = await this.special_group.createFolder(next_name, parent_id);
        if (second.ok) {
          return resolve({
            ok: true,
            msg: "ok", data: {
              fname: second.data.fname,
              id: second.data.id
            }
          })
        } else {
          return resolve({
            ok: false,
            msg: `second.msg = ${second.msg}`
          })
        }
      } else {
        return resolve({
          ok: false,
          msg: `first.msg = ${first.msg}`
        })
      }
    })
  }

  /**
   * 
   * @description 新建一个与文件同名的文件夹,然后上传单文件,然后用小文件覆盖来减少体积,返回相应的folder_id
   * @returns {Promise<{ok:Boolean,msg:String,data:{
   * folder_id:Number,
   * folder_name:String
   * }}>}
   * @param {String} file_path  必须是小于1GB的文件
   * @param {Number} cloud_parent_id 
   */
  uploadFileAsSingleFolder(file_path, cloud_parent_id) {
    return new Promise(resolve => [(async () => {
      let o_stats = await Toolbox.getStats(file_path);
      if (!o_stats.ok) {
        return resolve({
          ok: false,
          msg: `fail get stats:${o_stats.msg}`
        })
      }
      if (o_stats.stats.size > 1 * 1024 * 1024 * 1024) {
        return resolve({
          ok: false,
          msg: `size>1GB::${file_path}`
        })
      }
      let parsed_filepath = path.parse(file_path);
      let o_folder = await this.createFolder_GoodNoConflict(parsed_filepath.name, cloud_parent_id);
      if (!o_folder.ok) {
        return resolve({
          ok: false,
          msg: `create folder fail:${o_folder.msg}`
        })
      }
      let o_up = await this.special_group.uploadFileToKs3(file_path);
      if (!o_up.ok) {
        return resolve({
          ok: false,
          msg: `upload ks3 fail:${o_up.msg}`
        })
      }
      let defName = parsed_filepath.base;
      if (parsed_filepath.base == parsed_filepath.name
        && parsed_filepath.base.startsWith(".")) {
        defName = "NULL" + defName;
      }
      if (defName.length > 240) {
        defName = `${defName.substr(0, 240 - parsed_filepath.ext.length)}${parsed_filepath.ext}`
      }
      let o_createFile = await this.special_group.createFile_newfile(o_folder.data.id, defName,
        o_up.data.newfilename, o_up.data.etag, o_stats.stats.size);
      if (!o_createFile.ok) {
        return resolve({
          ok: false,
          msg: `create fileid fail:${o_createFile.msg}`
        })
      }
      let dummyFilePath = path.join(__dirname,
        `../../DUMMY/${rn({ min: 1, max: 6, integer: true })}.txt`)
      let upDummy = await this.special_group.uploadFileToKs3(dummyFilePath, path.extname(file_path));
      if (!upDummy.ok) {
        return resolve({
          ok: false,
          msg: `dummy file upload to ks3 fail:${upDummy.msg}`
        })
      }
      let dummyVersion = await this.special_group.createFile_new_version(o_createFile.data.id,
        upDummy.data.newfilename, upDummy.data.etag);
      if (!dummyVersion.ok) {
        return resolve({
          ok: false,
          msg: `fail to create fake version dummy:${dummyVersion.msg}`
        })
      }
      return resolve({
        ok: true,
        msg: "ok",
        data: {
          folder_id: o_folder.data.id,
          folder_name: o_folder.data.fname
        }
      })

    })()])
  }

  /**
   * @returns {Promise<{ok:Boolean,msg:String,data:{
    * folder_id:Number,
    * folder_name:String
    * }}>}
   * @param {String} folder_path 
   * @param {Number} cloud_parent_id 
   */
  uploadEntireFolder(folder_path, cloud_parent_id) {
    return new Promise(async resolve => {
      let o_list = await Toolbox.safeListDir(folder_path);
      let biggerThan1GB = o_list.filter(e => e.stats.size > 1 * 1024 * 1024 * 1024);
      if (biggerThan1GB.length) {
        return resolve({
          ok: false,
          msg: `some file > 1GB:${biggerThan1GB.map(e => e.full_path).join(" ; ")}`
        })
      }
      let createFolder = await this.createFolder_GoodNoConflict(path.basename(folder_path), cloud_parent_id);
      if (!createFolder.ok) {
        return resolve({
          ok: false,
          msg: `create folder fail for ${folder_path}:${createFolder.msg}`
        })
      }
      let subfolders = o_list.filter(e => e.stats.isDirectory());
      for (let d of subfolders) {
        let up_sub = await this.uploadEntireFolder(d.full_path, createFolder.data.id);
        if (!up_sub.ok) {
          return resolve({
            ok: false,
            msg: `fail uping sub-folder(${d.full_path})::${up_sub.msg}`
          })
        }
      }
      let small_files = o_list.filter(e => e.stats.size <= 10 * 1024 * 1024);
      let parallelCount = small_files.length ? ($ => {
        let sizeAverage = small_files.map(e => e.stats.size).reduce((a, b) => a + b) / (small_files.length);
        sizeAverage = Math.ceil(sizeAverage);
        let r = Math.ceil(10 * 1024 * 1024 / sizeAverage)
        return r >= small_files.length ? small_files.length : r;
      })() : 1;
      const AtLeast = 5;
      if (parallelCount < AtLeast) {
        parallelCount = AtLeast;
      }
      let files = o_list.filter(e => e.stats.isFile());
      let tasks = files.map(f => async cb => {
        let o_upks3 = await this.special_group.uploadFileToKs3(f.full_path);
        if (!o_upks3.ok) {
          return cb(`upload ks3(${f.full_path})::${o_upks3.msg}`)
        }
        let parsed_filepath = path.parse(f.full_path);
        let defName = parsed_filepath.base;
        if (parsed_filepath.base == parsed_filepath.name
          && parsed_filepath.base.startsWith(".")) {
          defName = "NULL" + defName;
        }
        if (defName.length > 240) {
          let matched = defName.toLowerCase().match(/.part[0-9]+\.rar$/);
          if (matched && matched.index) {
            let part = defName.substr(matched.index);
            defName = `${defName.substr(0, 240 - part.length)}${part}`
          } else {
            defName = `${defName.substr(0, 240 - parsed_filepath.ext.length)}${parsed_filepath.ext}`
          }
        }
        let o_createFile = await this.special_group.createFile_newfile(createFolder.data.id,
          defName, o_upks3.data.newfilename, o_upks3.data.etag, f.stats.size);
        if (!o_createFile.ok) {
          return cb(`create file ${f.full_path}::${o_createFile.msg}`)
        }
        let dummyPath = path.join(__dirname,
          `../../DUMMY/${rn({ min: 1, max: 6, integer: true })}.txt`)
        let upDummy = await this.special_group.uploadFileToKs3(dummyPath, path.extname(f.full_path));
        if (!upDummy.ok) {
          return cb(`dummy up fail(${f.full_path}):${upDummy.msg}`)
        }

        let dummyVersion = await this.special_group.createFile_new_version(o_createFile.data.id,
          upDummy.data.newfilename, upDummy.data.etag);
        if (!dummyVersion.ok) {
          return cb(`dummy version for ${f.full_path} FAIL:${dummyVersion.msg},dummyPath is ${dummyPath}`)
        }
        cb();
      });
      runParallel(tasks, parallelCount, (err) => {
        if (err) {
          return resolve({
            ok: false,
            msg: util.isError(err) ? err.message : util.inspect(err)
          })
        }
        return resolve({
          ok: true,
          msg: "ok",
          data: {
            folder_name: createFolder.data.fname,
            folder_id: createFolder.data.id
          }
        })
      })

    })
  }

  /**
   * @todo
   * @description 用于可以重复利用fileid,不关心filename的情况,比如保存种子或者html/txt文件,然后返回这次的file_id和history_id
   * @param {String} file_path 
   * @param {Number} cloud_parent_id_CONST 
   */
  uploadFile_returnHistory(file_path, cloud_parent_id_CONST) {
    /**
     * @todo 未完成
     */
  }


}



module.exports = {
  SpecialGroup
}